package org.anderground.editor;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.anderground.core.Logger;

/**
 * Reads classes from jar file.
 * 
 * @author Eldar Abusalimov
 * @author Ilya Shurigyn
 */
public class ClassReader {

	/**
	 * @author Ilya Shurigyn
	 */
	public static interface OnSuitableClassFoundCallback<T> {

		Class<T> getSuitableClass();

		void onSuitableClassFound(Class<? extends T> cls);

	}

	/**
	 * Reads the specified file and call proper listener for each found class.
	 * If the {@code file} represents a directory then lists it recursively and
	 * reads each file in it.
	 * 
	 * @param file
	 *            the file or directory to load classes from
	 * @param classFoundCallbacks
	 */
	public static List<Class<?>> loadClasses(File file,
			OnSuitableClassFoundCallback<?>... classFoundCallbacks) {
		ArrayList<URL> urlsList = new ArrayList<URL>();
		List<String> classNames = new ArrayList<String>();
		listFiles(file, urlsList, classNames);

		URL[] urls = urlsList.toArray(new URL[urlsList.size()]);
		Thread currentThread = Thread.currentThread();
		ClassLoader classLoader = new URLClassLoader(urls, currentThread
				.getContextClassLoader());
		currentThread.setContextClassLoader(classLoader);

		return extractClasses(classNames, classFoundCallbacks);
	}

	/**
	 * If the specified {@code file} is a {@link JarFile} then converts its name
	 * to {@link URL} adding it to the specified {@code urlsList} and lists its
	 * entries for class names adding them to the specified {@code classNames}
	 * list. If the {@code file} represents a directory then lists it
	 * recursively and reads each file in it.
	 * 
	 * @param file
	 *            the file or a directory to list
	 * @param urlsList
	 *            the list of {@link URL}s to populate. Must be allocated by
	 *            caller
	 * @param classNames
	 *            the list of class names to populate. Must be allocated by
	 *            caller
	 * @throws IllegalArgumentException
	 *             if either {@code urlsList} or {@code classNames} is null
	 */
	private static void listFiles(File file, List<URL> urlsList,
			List<String> classNames) {
		if (urlsList == null || classNames == null) {
			throw new IllegalArgumentException(
					"urlsList and classNames must not be null");
		}
		if (file.isHidden()) {
			return;
		}
		if (file.isFile()) {
			URL url;
			JarFile jarFile;
			try {
				url = file.toURI().toURL();
				jarFile = new JarFile(file);
			} catch (MalformedURLException e) {
				// we'll be not able to add this file to our ClassLoader,
				// so bypass and return
				return;
			} catch (IOException e) {
				// cannot read the jar file content
				// so bypass and return
				return;
			}
			urlsList.add(url);
			Enumeration<JarEntry> entries = jarFile.entries();
			final String CLASS_FILE_SUFFIX = ".class";
			final char SLASH = '/';
			final char DOT = '.';
			while (entries.hasMoreElements()) {
				JarEntry element = entries.nextElement();
				if (element.isDirectory()) {
					continue;
				}
				String className = element.getName();
				if (!className.endsWith(CLASS_FILE_SUFFIX)) {
					continue;
				}
				className = className.substring(0, className.length()
						- CLASS_FILE_SUFFIX.length());
				className = className.replace(SLASH, DOT);
				classNames.add(className);
			}
		} else if (file.isDirectory()) {
			for (File f : file.listFiles()) {
				listFiles(f, urlsList, classNames);
			}
		}
	}

	/**
	 * Tries to load each class with names listed in {@code classNames} invoking
	 * {@link OnSuitableClassFoundCallback#onSuitableClassFound(Class)} if
	 * {@link OnSuitableClassFoundCallback#getSuitableClass()} is assignable
	 * from the loaded class.
	 * 
	 * @param classNames
	 *            the names of classes to load
	 * @param classFoundCallbacks
	 * @return the list of loaded classes
	 * 
	 * @see Class#forName(String)
	 * @see Class#isAssignableFrom(Class)
	 */
	@SuppressWarnings("unchecked")
	private static List<Class<?>> extractClasses(List<String> classNames,
			OnSuitableClassFoundCallback<?>[] classFoundCallbacks) {
		List<Class<?>> classList = new ArrayList<Class<?>>();
		ClassLoader classLoader = Thread.currentThread()
				.getContextClassLoader();

		for (String className : classNames) {
			try {
				Class<?> extractedClass = classLoader.loadClass(className);
				classList.add(extractedClass);
				for (OnSuitableClassFoundCallback c : classFoundCallbacks) {
					Class clazz = c.getSuitableClass();
					if (clazz.isAssignableFrom(extractedClass)) {
						c.onSuitableClassFound(extractedClass);
					}
				}
			} catch (ClassNotFoundException e) {
				Logger.w("(ClassNotFoundException) failed to extract "
						+ className);
			} catch (NoClassDefFoundError e) {
				Logger.w("(NoClassDefFoundError) failed to extract "
						+ className);
			}
		}
		Logger.i("successfully extracted " + classList.size() + " items");

		return classList;
	}

}
